declare let require: any;
declare let XDomainRequest: any;
declare let ActiveXObject: any;
// declare let Symbol: Symbol;

let context = window;
let doc, byTag, head, XHR2;

doc = document;
byTag = 'getElementsByTagName';
head = doc[byTag]('head')[0];

// if ('window' in context) {
//     doc = document;
//     byTag = 'getElementsByTagName';
//     head = doc[byTag]('head')[0];
// } else {
//     try {
//         XHR2 = require('xhr2')
//     } catch (ex) {
//         throw new Error('Peer dependency `xhr2` required! Please npm install xhr2')
//     }
// }


let httpsRe = /^http/
    , protocolRe = /(^\w+):\/\//
    , twoHundo = /^(20\d|1223)$/ //http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
    , readyState = 'readyState'
    , contentType = 'Content-Type'
    , requestedWith = 'X-Requested-With'
    , uniqid = 0
    , callbackPrefix = 'reqwest_' + (+new Date())
    , lastValue // data stored by the most recent JSONP callback
    , xmlHttpRequest = 'XMLHttpRequest'
    , xDomainRequest = 'XDomainRequest'
    , noop = function () {
}

    , isArray = typeof Array.isArray == 'function'
    ? Array.isArray
    : function (a) {
        return a instanceof Array
    }

    , defaultHeaders = {
    'contentType': 'application/x-www-form-urlencoded'
    , 'requestedWith': xmlHttpRequest
    , 'accept': {
        '*': 'text/javascript, text/html, application/xml, text/xml, */*'
        , 'xml': 'application/xml, text/xml'
        , 'html': 'text/html'
        , 'text': 'text/plain'
        , 'json': 'application/json, text/javascript'
        , 'js': 'application/javascript, text/javascript'
    }
}

    , xhr = function (o) {
    // is it x-domain
    if (o['crossOrigin'] === true) {
        let xhr = context[xmlHttpRequest] ? new XMLHttpRequest() : null;
        if (xhr && 'withCredentials' in xhr) {
            return xhr
        } else if (context[xDomainRequest]) {
            return new XDomainRequest()
        } else {
            throw new Error('Browser does not support cross-origin requests')
        }
    } else if (context[xmlHttpRequest]) {
        return new XMLHttpRequest()
    } else if (XHR2) {
        return new XHR2()
    } else {
        return new ActiveXObject('Microsoft.XMLHTTP')
    }
}
    , globalSetupOptions = {
    dataFilter: function (data, type) {
        return data
    }
};

function succeed(r) {
    let protocol: any = protocolRe.exec(r.url);
    protocol = (protocol && protocol[1]) || context.location.protocol;
    return httpsRe.test(protocol) ? twoHundo.test(r.request.status) : !!r.request.response
}

function handleReadyState(r, success, error) {
    return function () {
        // use _aborted to mitigate against IE err c00c023f
        // (can't read props on aborted request objects)
        if (r._aborted) return error(r.request);
        if (r._timedOut) return error(r.request, 'Request is aborted: timeout');
        if (r.request && r.request[readyState] == 4) {
            r.request.onreadystatechange = noop;
            if (succeed(r)) success(r.request);
            else
                error(r.request)
        }
    }
}

function setHeaders(http, o) {
    let headers = o['headers'] || {}
        , h;

    headers['Accept'] = headers['Accept']
        || defaultHeaders['accept'][o['type']]
        || defaultHeaders['accept']['*'];

    let isAFormData = typeof FormData !== 'undefined' && (o['data'] instanceof FormData);
    // breaks cross-origin requests with legacy browsers
    if (!o['crossOrigin'] && !headers[requestedWith]) headers[requestedWith] = defaultHeaders['requestedWith'];
    if (!headers[contentType] && !isAFormData) headers[contentType] = o['contentType'] || defaultHeaders['contentType'];
    for (h in headers)
        headers.hasOwnProperty(h) && 'setRequestHeader' in http && http.setRequestHeader(h, headers[h])
}

function setCredentials(http, o) {
    if (typeof o['withCredentials'] !== 'undefined' && typeof http.withCredentials !== 'undefined') {
        http.withCredentials = !!o['withCredentials']
    }
}

function generalCallback(data) {
    lastValue = data
}

function urlappend(url, s) {
    return url + (/\?/.test(url) ? '&' : '?') + s;
}

function handleJsonp(o, fn, err, url) {
    let reqId = uniqid++
        , cbkey = o['jsonpCallback'] || 'callback' // the 'callback' key
        , cbval = o['jsonpCallbackName'] || getcallbackPrefix(reqId)
        , cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
        , match = url.match(cbreg)
        , script = doc.createElement('script')
        , loaded = 0
        , isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1;

    if (match) {
        if (match[3] === '?') {
            url = url.replace(cbreg, '$1=' + cbval); // wildcard callback func name
        } else {
            cbval = match[3]; // provided callback func name
        }
    } else {
        url = urlappend(url, cbkey + '=' + cbval); // no callback details, add 'em
    }

    context[cbval] = generalCallback;

    script.type = 'text/javascript';
    script.src = url;
    script.async = true;
    if (typeof script.onreadystatechange !== 'undefined' && !isIE10) {
        // need this for IE due to out-of-order onreadystatechange(), binding script
        // execution to an event listener gives us control over when the script
        // is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
        script.htmlFor = script.id = '_reqwest_' + reqId
    }

    script.onload = script.onreadystatechange = function () {
        if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
            return false
        }
        script.onload = script.onreadystatechange = null;
        script.onclick && script.onclick();
        // Call the user callback with the last value stored and clean up values and scripts.
        fn(lastValue);
        lastValue = undefined;
        head.removeChild(script);
        loaded = 1
    };

    // Add the script to the DOM head
    head.appendChild(script);

    // Enable JSONP timeout
    return {
        abort: function () {
            script.onload = script.onreadystatechange = null;
            err({}, 'Request is aborted: timeout', {});
            lastValue = undefined;
            head.removeChild(script);
            loaded = 1
        }
    }
}

function getRequest(fn, err) {
    let o = this.o
        , method = (o['method'] || 'GET').toUpperCase()
        , url = typeof o === 'string' ? o : o['url']
        // convert non-string objects to query-string form unless o['processData'] is false
        , data = (o['processData'] !== false && o['data'] && typeof o['data'] !== 'string')
            ? toQueryString(o['data'])
            : (o['data'] || null)
        , http
        , sendWait = false;

    // if we're working on a GET request and we have data then we should append
    // query string to end of URL and not post data
    if ((o['type'] == 'jsonp' || method == 'GET') && data) {
        url = urlappend(url, data);
        data = null
    }

    if (o['type'] == 'jsonp') return handleJsonp(o, fn, err, url);

    // get the xhr from the factory if passed
    // if the factory returns null, fall-back to ours
    http = (o.xhr && o.xhr(o)) || xhr(o);

    http.open(method, url, o['async'] === false ? false : true);
    setHeaders(http, o);
    setCredentials(http, o);
    if (context[xDomainRequest] && http instanceof context[xDomainRequest]) {
        http.onload = fn;
        http.onerror = err;
        // NOTE: see
        // http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e
        http.onprogress = function () {
        };
        sendWait = true
    } else {
        http.onreadystatechange = handleReadyState(this, fn, err)
    }
    o['before'] && o['before'](http);
    if (sendWait) {
        setTimeout(function () {
            http.send(data)
        }, 200)
    } else {
        http.send(data)
    }
    return http
}

class Reqwest<T> {

    o;
    fn;
    request;

    private _fulfilled;
    private _aborted;
    private _responseArgs;
    private _erred;
    private _fulfillmentHandlers;
    private _errorHandlers;
    private _completeHandlers;

    constructor(o: T, fn?) {
        this.o = o;
        this.fn = fn;
        init.apply(this, arguments)
    }

    [Symbol.toStringTag];

    abort() {
        this._aborted = true;
        this.request.abort()
    }

    retry() {
        init.call(this, this.o, this.fn)
    }

    /**
     * Small deviation from the Promises A CommonJs specification
     * http://wiki.commonjs.org/wiki/Promises/A
     */

    /**
     * `then` will execute upon successful requests
     */
    then(success, fail) {
        success = success || function () {
            };
        fail = fail || function () {
            };
        if (this._fulfilled) {
            this._responseArgs.resp = success(this._responseArgs.resp)
        } else if (this._erred) {
            fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
        } else {
            this._fulfillmentHandlers.push(success);
            this._errorHandlers.push(fail)
        }
        return this
    };

    /**
     * `always` will execute whether the request succeeds or fails
     */
    always(fn) {
        if (this._fulfilled || this._erred) {
            fn(this._responseArgs.resp)
        } else {
            this._completeHandlers.push(fn)
        }
        return this
    };

    /**
     * `fail` will execute when the request fails
     */
    fail(fn) {
        if (this._erred) {
            fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
        } else {
            this._errorHandlers.push(fn)
        }
        return this
    };

    'catch'(fn) {
        return this.fail(fn)
    }


}

// function Reqwest(o, fn) {
//     this.o = o;
//     this.fn = fn;
//
//     init.apply(this, arguments)
// }

function setType(header) {
    // json, javascript, text/plain, text/html, xml
    if (header === null) return undefined; //In case of no content-type.
    if (header.match('json')) return 'json';
    if (header.match('javascript')) return 'js';
    if (header.match('text')) return 'html';
    if (header.match('xml')) return 'xml';
}

function init(o, fn) {

    this.url = typeof o == 'string' ? o : o['url'];
    this.timeout = null;

    // whether request has been fulfilled for purpose
    // of tracking the Promises
    this._fulfilled = false;
    // success handlers
    this._successHandler = function () {
    };
    this._fulfillmentHandlers = [];
    // error handlers
    this._errorHandlers = [];
    // complete (both success and fail) handlers
    this._completeHandlers = [];
    this._erred = false;
    this._responseArgs = {};

    let self = this;

    fn = fn || function () {
        };

    if (o['timeout']) {
        this.timeout = setTimeout(function () {
            timedOut()
        }, o['timeout'])
    }

    if (o['success']) {
        this._successHandler = function () {
            o['success'].apply(o, arguments)
        }
    }

    if (o['error']) {
        this._errorHandlers.push(function () {
            o['error'].apply(o, arguments)
        })
    }

    if (o['complete']) {
        this._completeHandlers.push(function () {
            o['complete'].apply(o, arguments)
        })
    }

    function complete(resp) {
        o['timeout'] && clearTimeout(self.timeout);
        self.timeout = null;
        while (self._completeHandlers.length > 0) {
            self._completeHandlers.shift()(resp)
        }
    }

    function success(resp) {
        let type = o['type'] || resp && setType(resp.getResponseHeader('Content-Type')); // resp can be undefined in IE
        resp = (type !== 'jsonp') ? self.request : resp;
        // use global data filter on response text
        let filteredResponse = globalSetupOptions.dataFilter(resp.responseText, type)
            , r = filteredResponse;
        try {
            resp.responseText = r
        } catch (e) {
            // can't assign this in IE<=8, just ignore
        }
        if (r) {
            switch (type) {
                case 'json':
                    try {
                        resp = JSON ? JSON.parse(r) : eval('(' + r + ')')
                    } catch (err) {
                        return error(resp, 'Could not parse JSON in response', err)
                    }
                    break;
                case 'js':
                    resp = eval(r);
                    break;
                case 'html':
                    resp = r;
                    break;
                case 'xml':
                    resp = resp.responseXML
                    && resp.responseXML.parseError // IE trololo
                    && resp.responseXML.parseError.errorCode
                    && resp.responseXML.parseError.reason
                        ? null
                        : resp.responseXML;
                    break
            }
        }

        self._responseArgs.resp = resp;
        self._fulfilled = true;
        fn(resp);
        self._successHandler(resp);
        while (self._fulfillmentHandlers.length > 0) {
            resp = self._fulfillmentHandlers.shift()(resp)
        }

        complete(resp)
    }

    function timedOut() {
        self._timedOut = true;
        self.request.abort()
    }

    function error(resp, msg, t) {
        resp = self.request;
        self._responseArgs.resp = resp;
        self._responseArgs.msg = msg;
        self._responseArgs.t = t;
        self._erred = true;
        while (self._errorHandlers.length > 0) {
            self._errorHandlers.shift()(resp, msg, t)
        }
        complete(resp)
    }

    this.request = getRequest.call(this, success, error)
}

// normalize newline variants according to spec -> CRLF
function normalize(s) {
    return s ? s.replace(/\r?\n/g, '\r\n') : ''
}

function serial(el, cb) {
    let n = el.name
        , t = el.tagName.toLowerCase()
        , optCb = function (o) {
        // IE gives value="" even where there is no value attribute
        // 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
        if (o && !o['disabled'])
            cb(n, normalize(o['attributes']['value'] && o['attributes']['value']['specified'] ? o['value'] : o['text']))
    }
        , ch, ra, val, i;

    // don't serialize elements that are disabled or without a name
    if (el.disabled || !n) return;

    switch (t) {
        case 'input':
            if (!/reset|button|image|file/i.test(el.type)) {
                ch = /checkbox/i.test(el.type);
                ra = /radio/i.test(el.type);
                val = el.value
                // WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
                ;
                (!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
            }
            break;
        case 'textarea':
            cb(n, normalize(el.value));
            break;
        case 'select':
            if (el.type.toLowerCase() === 'select-one') {
                optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
            } else {
                for (i = 0; el.length && i < el.length; i++) {
                    el.options[i].selected && optCb(el.options[i])
                }
            }
            break
    }
}

// collect up all form elements found from the passed argument elements all
// the way down to child elements; pass a '<form>' or form fields.
// called with 'this'=callback to use for serial() on each element
function eachFormElement() {
    let cb = this
        , e, i
        , serializeSubtags = function (e, tags) {
        let i, j, fa;
        for (i = 0; i < tags.length; i++) {
            fa = e[byTag](tags[i])
            for (j = 0; j < fa.length; j++) serial(fa[j], cb)
        }
    };

    for (i = 0; i < arguments.length; i++) {
        e = arguments[i];
        if (/input|select|textarea/i.test(e.tagName)) serial(e, cb);
        serializeSubtags(e, ['input', 'select', 'textarea'])
    }
}

// standard query string style serialization
function serializeQueryString() {
    return toQueryString(serializeArray.apply(null, arguments))
}

// { 'name': 'value', ... } style serialization
function serializeHash() {
    let hash = {};
    eachFormElement.apply(function (name, value) {
        if (name in hash) {
            hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]])
            hash[name].push(value)
        } else hash[name] = value
    }, arguments);
    return hash
}

// [ { name: 'name', value: 'value' }, ... ] style serialization
export function serializeArray() {
    let arr = [];
    eachFormElement.apply(function (name, value) {
        arr.push({name: name, value: value})
    }, arguments);
    return arr
}

export function serialize() {
    if (arguments.length === 0) return '';
    let opt, fn
        , args = Array.prototype.slice.call(arguments, 0);

    opt = args.pop();
    opt && opt.nodeType && args.push(opt) && (opt = null);
    opt && (opt = opt.type);

    if (opt == 'map') fn = serializeHash;
    else if (opt == 'array') fn = serializeArray;
    else fn = serializeQueryString;

    return fn.apply(null, args)
}

export function toQueryString(o, trad?) {
    let prefix, i
        , traditional = trad || false
        , s = []
        , enc = encodeURIComponent
        , add = function (key, value) {
        // If value is a function, invoke it and return its value
        value = ('function' === typeof value) ? value() : (value == null ? '' : value);
        s[s.length] = enc(key) + '=' + enc(value)
    };
    // If an array was passed in, assume that it is an array of form elements.
    if (isArray(o)) {
        for (i = 0; o && i < o.length; i++) add(o[i]['name'], o[i]['value'])
    } else {
        // If traditional, encode the "old" way (the way 1.3.2 or older
        // did it), otherwise encode params recursively.
        for (prefix in o) {
            if (o.hasOwnProperty(prefix)) buildParams(prefix, o[prefix], traditional, add)
        }
    }

    // spaces should be + according to spec
    return s.join('&').replace(/%20/g, '+')
}

function buildParams(prefix, obj, traditional, add) {
    let name, i, v
        , rbracket = /\[\]$/;

    if (isArray(obj)) {
        // Serialize array item.
        for (i = 0; obj && i < obj.length; i++) {
            v = obj[i];
            if (traditional || rbracket.test(prefix)) {
                // Treat each array item as a scalar.
                add(prefix, v)
            } else {
                buildParams(prefix + '[' + (typeof v === 'object' ? i : '') + ']', v, traditional, add)
            }
        }
    } else if (obj && obj.toString() === '[object Object]') {
        // Serialize object item.
        for (name in obj) {
            buildParams(prefix + '[' + name + ']', obj[name], traditional, add)
        }

    } else {
        // Serialize scalar item.
        add(prefix, obj)
    }
}

export function getcallbackPrefix(reqId) {
    return callbackPrefix
}

// jQuery and Zepto compatibility, differences can be remapped here so you can call
// .ajax.compat(options, callback)
export function compat(o, fn) {
    if (o) {
        o['type'] && (o['method'] = o['type']) && delete o['type'];
        o['dataType'] && (o['type'] = o['dataType']);
        o['jsonpCallback'] && (o['jsonpCallbackName'] = o['jsonpCallback']) && delete o['jsonpCallback'];
        o['jsonp'] && (o['jsonpCallback'] = o['jsonp'])
    }
    return new Reqwest(o, fn)
}

export function ajaxSetup(options) {
    options = options || {};
    for (let k in options) {
        globalSetupOptions[k] = options[k]
    }
}

export default Reqwest;
